/*******************************************************************************
 * Copyright (c) 2007, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.services;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.commands.util.Tracing;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionInfo;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.ISources;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.misc.Policy;
import org.eclipse.ui.services.IEvaluationReference;
import org.eclipse.ui.services.IEvaluationService;

/**
 * 
 */
public class EvaluationAuthority extends ExpressionAuthority {

	/**
	 * 
	 */
	private static final String COMPONENT = "EVALUATION"; //$NON-NLS-1$

	/**
	 * A bucket sort of the evaluation references based on source priority. Each
	 * reference will appear only once per set, but may appear in multiple sets.
	 * If no references are defined for a particular priority level, then the
	 * array at that index will only contain <code>null</code>.
	 */
	private final Map cachesBySourceName = new HashMap();
	private ListenerList serviceListeners = new ListenerList();
	private int notifying = 0;

	// private final Map cachesByExpression = new HashMap();

	public void addEvaluationListener(IEvaluationReference ref) {
		// we update the source priority bucket sort of activations.
		String[] sourceNames = getNames(ref);
		for (int i = 0; i < sourceNames.length; i++) {
			Map cachesByExpression = (HashMap) cachesBySourceName
					.get(sourceNames[i]);
			if (cachesByExpression == null) {
				cachesByExpression = new HashMap(1);
				cachesBySourceName.put(sourceNames[i], cachesByExpression);
			}
			final Expression expression = ref.getExpression();
			Set caches = (Set) cachesByExpression.get(expression);
			if (caches == null) {
				caches = new HashSet();
				cachesByExpression.put(expression, caches);
			}
			caches.add(ref);
		}

		boolean result = evaluate(ref);
		firePropertyChange(ref, null, valueOf(result));
	}

	private Boolean valueOf(boolean result) {
		return result ? Boolean.TRUE : Boolean.FALSE;
	}

	private String[] getNames(IEvaluationReference ref) {
		ExpressionInfo info = new ExpressionInfo();
		ref.getExpression().collectExpressionInfo(info);
		ArrayList allNames = new ArrayList(Arrays.asList(info
				.getAccessedVariableNames()));
		if (info.hasDefaultVariableAccess()) {
			allNames.add(ISources.ACTIVE_CURRENT_SELECTION_NAME);
		}
		allNames.addAll(Arrays.asList(info.getAccessedPropertyNames()));
		return (String[]) allNames.toArray(new String[allNames.size()]);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.internal.services.ExpressionAuthority#sourceChanged(int)
	 */
	protected void sourceChanged(int sourcePriority) {
		// no-op, we want the other one
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.ui.internal.services.ExpressionAuthority#sourceChanged(java.lang.String[])
	 */
	protected void sourceChanged(String[] sourceNames) {
		startSourceChange(sourceNames);
		try {
			// evaluations to recompute
			for (int i = 0; i < sourceNames.length; i++) {
				Map cachesByExpression = (HashMap) cachesBySourceName
						.get(sourceNames[i]);
				if (cachesByExpression != null) {
					Collection v = cachesByExpression.values();
					Set[] expressionCaches = (Set[]) v
							.toArray(new Set[v.size()]);
					for (int j = 0; j < expressionCaches.length; j++) {
						if (expressionCaches[j].size() > 0) {
							EvaluationReference[] refs = (EvaluationReference[]) expressionCaches[j]
									.toArray(new EvaluationReference[expressionCaches[j]
											.size()]);
							refsWithSameExpression(refs);
						}
					}
				}
			}
		} finally {
			endSourceChange(sourceNames);
		}
	}

	/**
	 * This will evaluate all refs with the same expression.
	 * 
	 * @param refs
	 */
	private void refsWithSameExpression(EvaluationReference[] refs) {
		int k = 0;
		while (k < refs.length && !refs[k].isPostingChanges()) {
			k++;
		}
		if (k >= refs.length) {
			return;
		}
		EvaluationReference ref = refs[k];
		boolean oldValue = evaluate(ref);
		ref.clearResult();
		final boolean newValue = evaluate(ref);
		if (oldValue != newValue) {
			firePropertyChange(ref, valueOf(oldValue), valueOf(newValue));
		}
		for (k++; k < refs.length; k++) {
			ref = refs[k];
			// this is not as expensive as it looks
			if (ref.isPostingChanges()) {
				oldValue = evaluate(ref);
				if (oldValue != newValue) {
					ref.setResult(newValue);
					firePropertyChange(ref, valueOf(oldValue),
							valueOf(newValue));
				}
			}
		}
	}

	/**
	 * @param sourceNames
	 */
	private void startSourceChange(final String[] sourceNames) {
		if (Policy.DEBUG_SOURCES) {
			Tracing.printTrace(COMPONENT, "start source changed: " //$NON-NLS-1$
					+ Arrays.asList(sourceNames));
		}
		notifying++;
		if (notifying == 1) {
			fireServiceChange(IEvaluationService.PROP_NOTIFYING, Boolean.FALSE,
					Boolean.TRUE);
		}
	}

	/**
	 * @param sourceNames
	 */
	private void endSourceChange(final String[] sourceNames) {
		if (Policy.DEBUG_SOURCES) {
			Tracing.printTrace(COMPONENT, "end source changed: " //$NON-NLS-1$
					+ Arrays.asList(sourceNames));
		}
		if (notifying == 1) {
			fireServiceChange(IEvaluationService.PROP_NOTIFYING, Boolean.TRUE,
					Boolean.FALSE);
		}
		notifying--;
	}

	/**
	 * @param ref
	 */
	public void removeEvaluationListener(IEvaluationReference ref) {
		// Next we update the source priority bucket sort of activations.
		String[] sourceNames = getNames(ref);
		for (int i = 0; i < sourceNames.length; i++) {
			Map cachesByExpression = (HashMap) cachesBySourceName
					.get(sourceNames[i]);
			if (cachesByExpression != null) {
				Set caches = (Set) cachesByExpression.get(ref.getExpression());
				if (caches != null) {
					caches.remove(ref);
					if (caches.isEmpty()) {
						cachesByExpression.remove(ref.getExpression());
					}
				}
				if (cachesByExpression.isEmpty()) {
					cachesBySourceName.remove(sourceNames[i]);
				}
			}
		}
		boolean result = evaluate(ref);
		firePropertyChange(ref, valueOf(result), null);
	}

	/**
	 * @param ref
	 * @param oldValue
	 * @param newValue
	 */
	private void firePropertyChange(IEvaluationReference ref, Object oldValue,
			Object newValue) {
		ref.getListener().propertyChange(
				new PropertyChangeEvent(ref, ref.getProperty(), oldValue,
						newValue));
	}

	private void fireServiceChange(final String property,
			final Object oldValue, final Object newValue) {
		Object[] listeners = serviceListeners.getListeners();
		for (int i = 0; i < listeners.length; i++) {
			final IPropertyChangeListener listener = (IPropertyChangeListener) listeners[i];
			SafeRunner.run(new ISafeRunnable() {
				public void handleException(Throwable exception) {
					WorkbenchPlugin.log(exception);
				}

				public void run() throws Exception {
					listener.propertyChange(new PropertyChangeEvent(
							EvaluationAuthority.this, property, oldValue,
							newValue));
				}
			});
		}
	}

	/**
	 * @param listener
	 */
	public void addServiceListener(IPropertyChangeListener listener) {
		serviceListeners.add(listener);
	}

	/**
	 * @param listener
	 */
	public void removeServiceListener(IPropertyChangeListener listener) {
		serviceListeners.remove(listener);
	}

	/**
	 * <p>
	 * Bug 95792. A mechanism by which the key binding architecture can force an
	 * update of the handlers (based on the active shell) before trying to
	 * execute a command. This mechanism is required for GTK+ only.
	 * </p>
	 * <p>
	 * DO NOT CALL THIS METHOD.
	 * </p>
	 */
	final void updateShellKludge() {
		updateCurrentState();
		sourceChanged(new String[] { ISources.ACTIVE_SHELL_NAME });
	}

	/**
	 * Returns the currently active shell.
	 * 
	 * @return The currently active shell; may be <code>null</code>.
	 */
	final Shell getActiveShell() {
		return (Shell) getVariable(ISources.ACTIVE_SHELL_NAME);
	}

}
